[templateId].tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useParams } from 'common'
  4. import Link from 'next/link'
  5. import { useRouter } from 'next/router'
  6. import { useEffect } from 'react'
  7. import { useForm } from 'react-hook-form'
  8. import { toast } from 'sonner'
  9. import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui'
  10. import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
  11. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  12. import { PageContainer } from 'ui-patterns/PageContainer'
  13. import {
  14. PageHeader,
  15. PageHeaderAside,
  16. PageHeaderBreadcrumb,
  17. PageHeaderDescription,
  18. PageHeaderMeta,
  19. PageHeaderSummary,
  20. PageHeaderTitle,
  21. } from 'ui-patterns/PageHeader'
  22. import {
  23. PageSection,
  24. PageSectionContent,
  25. PageSectionMeta,
  26. PageSectionSummary,
  27. PageSectionTitle,
  28. } from 'ui-patterns/PageSection'
  29. import {
  30. BreadcrumbItem,
  31. BreadcrumbLink,
  32. BreadcrumbList,
  33. BreadcrumbPage,
  34. BreadcrumbSeparator,
  35. } from 'ui/src/components/shadcn/ui/breadcrumb'
  36. import * as z from 'zod'
  37. import { TEMPLATES_SCHEMAS } from '@/components/interfaces/Auth/EmailTemplates/AuthTemplatesValidation'
  38. import { slugifyTitle } from '@/components/interfaces/Auth/EmailTemplates/EmailTemplates.utils'
  39. import { TemplateEditor } from '@/components/interfaces/Auth/EmailTemplates/TemplateEditor'
  40. import AuthLayout from '@/components/layouts/AuthLayout/AuthLayout'
  41. import { DefaultLayout } from '@/components/layouts/DefaultLayout'
  42. import { DocsButton } from '@/components/ui/DocsButton'
  43. import { NoPermission } from '@/components/ui/NoPermission'
  44. import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
  45. import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
  46. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  47. import { DOCS_URL } from '@/lib/constants'
  48. import type { NextPageWithLayout } from '@/types'
  49. const TemplatePage: NextPageWithLayout = () => {
  50. return <RedirectToTemplates />
  51. }
  52. const RedirectToTemplates = () => {
  53. const router = useRouter()
  54. const { templateId, ref } = router.query
  55. const { ref: projectRef } = useParams()
  56. const { can: canReadAuthSettings, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
  57. PermissionAction.READ,
  58. 'custom_config_gotrue'
  59. )
  60. const { can: canUpdateConfig } = useAsyncCheckPermissions(
  61. PermissionAction.UPDATE,
  62. 'custom_config_gotrue'
  63. )
  64. const { data: authConfig, isPending: isLoadingConfig } = useAuthConfigQuery({ projectRef })
  65. const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation({
  66. onError: (error) => {
  67. toast.error(`Failed to update settings: ${error?.message}`)
  68. },
  69. onSuccess: () => {
  70. toast.success('Successfully updated settings')
  71. },
  72. })
  73. // Find template whose slug matches the URL slug
  74. const template =
  75. templateId && typeof templateId === 'string'
  76. ? TEMPLATES_SCHEMAS.find((template) => slugifyTitle(template.title) === templateId)
  77. : null
  78. // Convert templateId slug to one lowercase word to match docs anchor tag
  79. const templateIdForDocs =
  80. typeof templateId === 'string' ? templateId.replace(/-/g, '').toLowerCase() : ''
  81. // Determine if this is a security notification template
  82. const isSecurityTemplate = template?.misc?.emailTemplateType === 'security'
  83. // Get the enabled key for security templates
  84. const templateEnabledKey = isSecurityTemplate
  85. ? (`MAILER_NOTIFICATIONS_${template.id?.replace('_NOTIFICATION', '')}_ENABLED` as string)
  86. : null
  87. const showConfigurationSection = isSecurityTemplate && templateEnabledKey
  88. // Create form schema for security templates
  89. const TemplateFormSchema = templateEnabledKey
  90. ? z.object({
  91. [templateEnabledKey]: z.boolean(),
  92. })
  93. : z.object({})
  94. const defaultValues = templateEnabledKey
  95. ? {
  96. [templateEnabledKey]: authConfig
  97. ? Boolean(authConfig[templateEnabledKey as keyof typeof authConfig])
  98. : false,
  99. }
  100. : {}
  101. const templateForm = useForm<z.infer<typeof TemplateFormSchema>>({
  102. resolver: zodResolver(TemplateFormSchema as any),
  103. defaultValues,
  104. })
  105. const onSubmit = (values: z.infer<typeof TemplateFormSchema>) => {
  106. if (!projectRef) return console.error('Project ref is required')
  107. updateAuthConfig({ projectRef: projectRef, config: { ...values }, skipInvalidation: true })
  108. }
  109. useEffect(() => {
  110. if (authConfig && templateEnabledKey) {
  111. templateForm.reset({
  112. [templateEnabledKey]: Boolean(authConfig[templateEnabledKey as keyof typeof authConfig]),
  113. })
  114. }
  115. // eslint-disable-next-line react-hooks/exhaustive-deps
  116. }, [authConfig, templateEnabledKey])
  117. if (isPermissionsLoaded && !canReadAuthSettings) {
  118. return <NoPermission isFullPage resourceText="access your project's email settings" />
  119. }
  120. if (!templateId) {
  121. return null
  122. }
  123. // Show error if templateId is invalid or template is not found
  124. if (!template) {
  125. return (
  126. <div className="flex h-full w-full items-center justify-center">
  127. <Admonition
  128. className="max-w-md"
  129. type="default"
  130. title="Unable to find template"
  131. description={`${templateId ? `The template "${templateId}"` : 'This template'} doesn’t seem to exist.`}
  132. >
  133. <Button asChild type="default" className="mt-2">
  134. <Link href={`/project/${ref}/auth/templates`}>Head back</Link>
  135. </Button>
  136. </Admonition>
  137. </div>
  138. )
  139. }
  140. return (
  141. <>
  142. <PageHeader size="default">
  143. <PageHeaderBreadcrumb>
  144. <BreadcrumbList>
  145. <BreadcrumbItem>
  146. <BreadcrumbLink asChild>
  147. <Link href={`/project/${ref}/auth/templates`}>Emails</Link>
  148. </BreadcrumbLink>
  149. </BreadcrumbItem>
  150. <BreadcrumbSeparator />
  151. <BreadcrumbItem>
  152. <BreadcrumbPage>{template.title}</BreadcrumbPage>
  153. </BreadcrumbItem>
  154. </BreadcrumbList>
  155. </PageHeaderBreadcrumb>
  156. <PageHeaderMeta>
  157. <PageHeaderSummary>
  158. <PageHeaderTitle>{template.title}</PageHeaderTitle>
  159. <PageHeaderDescription>
  160. {template.purpose || 'Configure and customize email templates.'}
  161. </PageHeaderDescription>
  162. </PageHeaderSummary>
  163. <PageHeaderAside>
  164. <DocsButton
  165. href={`${DOCS_URL}/guides/local-development/customizing-email-templates#${isSecurityTemplate ? 'security' : 'auth'}emailtemplate${templateIdForDocs}`}
  166. />
  167. </PageHeaderAside>
  168. </PageHeaderMeta>
  169. </PageHeader>
  170. <PageContainer size="default" className="pb-16">
  171. {!isPermissionsLoaded || isLoadingConfig ? (
  172. <PageSection>
  173. <PageSectionContent>
  174. <GenericSkeletonLoader />
  175. </PageSectionContent>
  176. </PageSection>
  177. ) : (
  178. <>
  179. {showConfigurationSection && (
  180. <PageSection>
  181. <PageSectionMeta>
  182. <PageSectionSummary>
  183. <PageSectionTitle>Configuration</PageSectionTitle>
  184. </PageSectionSummary>
  185. </PageSectionMeta>
  186. <PageSectionContent>
  187. <Form {...templateForm}>
  188. <form onSubmit={templateForm.handleSubmit(onSubmit)} className="space-y-4">
  189. <Card>
  190. <CardContent>
  191. <FormField
  192. control={templateForm.control}
  193. name={templateEnabledKey as keyof z.infer<typeof TemplateFormSchema>}
  194. render={({ field }) => (
  195. <FormItemLayout
  196. layout="flex-row-reverse"
  197. label="Enable notification"
  198. description="Send this email to users when triggered"
  199. >
  200. <FormControl>
  201. <Switch
  202. checked={field.value}
  203. onCheckedChange={field.onChange}
  204. disabled={!canUpdateConfig}
  205. />
  206. </FormControl>
  207. </FormItemLayout>
  208. )}
  209. />
  210. </CardContent>
  211. <CardFooter className="justify-end space-x-2">
  212. {templateForm.formState.isDirty && (
  213. <Button type="default" onClick={() => templateForm.reset()}>
  214. Cancel
  215. </Button>
  216. )}
  217. <Button
  218. type="primary"
  219. htmlType="submit"
  220. disabled={
  221. !canUpdateConfig ||
  222. isUpdatingConfig ||
  223. !templateForm.formState.isDirty
  224. }
  225. loading={isUpdatingConfig}
  226. >
  227. Save changes
  228. </Button>
  229. </CardFooter>
  230. </Card>
  231. </form>
  232. </Form>
  233. </PageSectionContent>
  234. </PageSection>
  235. )}
  236. <PageSection>
  237. {showConfigurationSection && (
  238. <PageSectionMeta>
  239. <PageSectionSummary>
  240. <PageSectionTitle>Content</PageSectionTitle>
  241. </PageSectionSummary>
  242. </PageSectionMeta>
  243. )}
  244. <PageSectionContent>
  245. <Card>
  246. <TemplateEditor template={template} />
  247. </Card>
  248. </PageSectionContent>
  249. </PageSection>
  250. </>
  251. )}
  252. </PageContainer>
  253. </>
  254. )
  255. }
  256. TemplatePage.getLayout = (page) => (
  257. <DefaultLayout>
  258. <AuthLayout title="Emails">{page}</AuthLayout>
  259. </DefaultLayout>
  260. )
  261. export default TemplatePage